package edu.mtu.citizenscience.ethnography.database;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.List;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import android.util.SparseArray;

/**
 * A Service that controls persistent storage for the Ethnography app. The service's
 * methods handle both database and filesystem operations.
 * 
 * @author Nicholas Blecha
 *
 */
public class InterviewDataService extends Service {

	private static final String LOG_TAG = "Interview Data Service";

	private static final String APP_DIR = "ethnography/";
	private final IBinder mBinder = new InterviewBinder();

	private InterviewDataSource dataSource;

	// Used to store "active" interviews, i.e. interviews that are being worked with
	// so that there is only a single copy of any one interview
	private SparseArray<Interview> activeInterviews;

	public class InterviewBinder extends Binder {
		public InterviewDataService getService() {
			return InterviewDataService.this;
		}
	}

	@Override
	public IBinder onBind(Intent intent) {
		activeInterviews = new SparseArray<Interview>();
		dataSource = new InterviewDataSource(this.getApplicationContext());
		dataSource.open();
		return mBinder;
	}

	/**
	 * Creates and returns an Interview object representing a new, blank interview.
	 * Also creates a row in the database for the interview (thus generating an
	 * interview ID), and creates/generates a path for the interview's media files;
	 * @return The new Interview object
	 * @throws IOException if external storage is not available for writing
	 */
	public Interview newInterview() throws IOException {
		checkExternalStorage();
		Interview interview = new Interview();
		interview.setDate(Calendar.getInstance().getTime());
		int result = dataSource.addInterview(interview);
		if (result > 0) {
			interview.setID(result);

			// Create the interview folders
			String folderName = generateInterviewFolderName(interview);
			interview.setMediaPath(folderName);
			dataSource.updateInterview(interview);
			//String folderPath = getAppFolder().getAbsolutePath() + "/" + folderName;
			String audioPath = getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + folderName;
			File audioFolder = new File(audioPath);
			if (audioFolder.exists()) {
				Log.d(LOG_TAG, "Audio folder exists");
			}
			if (!audioFolder.mkdir()) {
				// TODO: error handling
				Log.e(LOG_TAG, "Failed to create audio folder");
			}

			String photoPath = getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + folderName;
			File photoFolder = new File(photoPath);
			if (photoFolder.exists()) {
				Log.d(LOG_TAG, "Photo folder exists");
			}
			if (!photoFolder.mkdir()) {
				Log.e(LOG_TAG, "Failed to create photo folder");
			}

			// Store the interview in the list of active interviews
			// to keep a singleton
			activeInterviews.append(interview.getID(), interview);
		} else {
			// TODO: An error occured; handle it
		}
		return interview;
	}

	/**
	 * Takes the data in the specified {@link Interview} object and writes it
	 * to the database. It also "recalculates" what the name of the folder used
	 * to store media files for the interview should be, and if it has changed,
	 * it renames it. If there are any audio or photo files open, this may result
	 * in undefined behavior. Therefore, this method should <b>only</b> be called
	 * when there are no audio or photo files associated with the given Interview
	 * currently open.
	 * 
	 * The formula for determining an interview folder's name is 
	 * {@code subjectFirstName_subjectLastName_date}. If any of these data fields are
	 * blank, it will be skipped, and if the formula would result in a duplicate name,
	 * it appends a _n where n is an integer.
	 * 
	 * @param interview The interview data to store to the database
	 * @throws IOException If external storage is unavailable.
	 * @throws NullPointerException If {@code interview} is null.
	 */
	public void saveInterview(Interview interview) throws IOException {
		checkExternalStorage();
		if (interview == null) {
			throw new IllegalArgumentException("interview may not be null");
		}
		if (interview.isModified()) {
			String newFolderName = generateInterviewFolderName(interview);

			File oldAudioFolder = new File(getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + interview.getMediaPath());
			File newAudioFolder = new File(getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + newFolderName);
			oldAudioFolder.renameTo(newAudioFolder);

			File oldPhotoFolder = new File(getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + interview.getMediaPath());
			File newPhotoFolder = new File(getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + newFolderName);
			oldPhotoFolder.renameTo(newPhotoFolder);

			interview.setMediaPath(newFolderName);
			interview.resetModified();
		}
		// Remove the database from the active set
		activeInterviews.remove(interview.getID());
		dataSource.updateInterview(interview);
	}

	/**
	 * Takes the data in the specified {@link Interview} and writes it to the database
	 * without updating the filesystem. 
	 * @param interview
	 */
	public void saveInterviewWithoutRename(Interview interview) {
		if (interview == null) {
			throw new IllegalArgumentException("interview may not be null");
		}
		dataSource.updateInterview(interview);
	}

	/**
	 * Retrieves a specific {@link Interview} by id. This method creates an Interview
	 * object from the stored row in the database. It then checks the filesystem to 
	 * verify that the media folders are present and writable. Finally, it caches it
	 * so that future queries for the same id return the same object; this both speeds
	 * access time and ensures that only a single object representing a given Interview
	 * exists at any given time.
	 * 
	 * @param id The row ID of the interview to look up
	 * @return An Interview object containing the stored data for that row in the database,
	 * or {@code null} if the given id does not correspond to any row.
	 */
	public Interview getInterview(int id) {
		// Check if the interview is in the "active" set
		Interview interview = activeInterviews.get(id);
		if (interview == null) {
			// If not, grab it from the database and store it in the active set
			interview = dataSource.getInterviewByID(id);
			if (interview != null) {
				checkInterviewMedia(interview);
				activeInterviews.append(interview.getID(), interview);
			}
		}
		return interview;
	}

	/**
	 * Retrieves a {@link List} of all interviews currently stored in the database.
	 * These interviews are not cached; they should therefore not be assumed to reflect
	 * uncommited changes and should be refreshed if data may have been changed or committed.
	 * @return A List of all Intervews stored in the database. If there are no Interviews,
	 * returns an empty list.
	 */
	public List<Interview> getAllInterviews() {
		List<Interview> interviews = dataSource.getAllInterviews();
		for (Interview interview : interviews) {
			checkInterviewMedia(interview);
		}
		return interviews;
	}

	private static void checkInterviewMedia(Interview interview) {
		File audioFolder;
		try {
			audioFolder = new File(getAppFolder(Environment.DIRECTORY_MUSIC) + "/" + interview.getMediaPath());
		} catch (IOException e) {
			interview.setAudioFolderStatus(InterviewFileStatus.NOT_FOUND);
			interview.setPhotoFolderStatus(InterviewFileStatus.NOT_FOUND);
			return;
		}
		if (!audioFolder.exists()) {
			interview.setAudioFolderStatus(InterviewFileStatus.NOT_FOUND);
		} else if (!audioFolder.canWrite()) {
			interview.setAudioFolderStatus(InterviewFileStatus.READ_ONLY);
		} else {
			interview.setAudioFolderStatus(InterviewFileStatus.FOUND);
		}
		File photoFolder;
		try {
			photoFolder = new File(getAppFolder(Environment.DIRECTORY_PICTURES) + "/" + interview.getMediaPath());
		} catch (IOException e) {
			interview.setPhotoFolderStatus(InterviewFileStatus.NOT_FOUND);
			return;
		}
		if (!photoFolder.exists()) {
			interview.setPhotoFolderStatus(InterviewFileStatus.NOT_FOUND);
		} else if (!photoFolder.canWrite()) {
			interview.setPhotoFolderStatus(InterviewFileStatus.READ_ONLY);
		} else {
			interview.setPhotoFolderStatus(InterviewFileStatus.FOUND);
		}
	}

	/**
	 * Removes an Interview from the database, and deletes all associated media files.
	 * This method not only removes the Interview from the database, but also removes
	 * the entries for any associated Audio or Photo files, and deletes those files.
	 * It will generate an exception if external storage is not writable.
	 * @param interview The Interview to remove.
	 * @throws IOException If external storage is not mounted, or is not writable
	 * @throws IllegalArgumentException If {@code interview} is null.
	 */
	public void removeInterview(Interview interview) throws IOException {
		checkExternalStorage();
		if (interview == null) {
			throw new IllegalArgumentException("interview may not be null");
		}
		activeInterviews.remove(interview.getID());
		// Remove rows from the database
		dataSource.deleteRow(interview.getID());

		removeMediaFolders(interview);
	}
	
	public void removeInterviewAndKeepMedia(Interview interview) {
		if (interview == null) {
			throw new IllegalArgumentException("interview may not be null");
		}
		activeInterviews.remove(interview.getID());
		dataSource.deleteRow(interview.getID());
	}

	private static String generateInterviewFolderName(Interview interview) throws IOException {
		checkExternalStorage();
		String name = "";
		String storyTitle = interview.getStoryTitle();
		if (!storyTitle.equals("")) {
			name += storyTitle;
		} else {
			Interview.Name subjectName = interview.getSubjectName();
			// Append the first name, if it exists
			if (!subjectName.getFirstName().equals("")) {
				name += interview.getSubjectName().getFirstName();
			}
			// Append the last name, if it exists
			if (!subjectName.getLastName().equals("")) {
				if (!name.equals("")) {
					name += "_";
				}
				name += subjectName.getLastName();
			}
		}
		// Append the date, if it exists
		if (interview.getDate() != null) {
			if (!name.equals("")) {
				name += "_";
			}
			name += DateFormat.getDateInstance().format(interview.getDate());
		}
		// Create a generic name, if nothing exists
		if (name.equals("")) {
			name = "Interview" + String.valueOf(interview.getID());
		}

		// Verify our file doesn't exist, and append numbers to the end until
		// we get one that doesn't
		String folderPath = getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + name;
		File folder = new File(folderPath);
		int i = 0;
		while (folder.exists()) {
			String appendedPath = folderPath + "_" + String.valueOf(i);
			folder = new File(appendedPath);
			if (folder.exists()) {
				i++;
			} else {
				name += "_" + String.valueOf(i);
			}
		}
		name += "/";
		return name;
	}

	private static void removeMediaFolders(Interview interview) {
		File audioFolder;
		try {
			audioFolder = new File(getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + interview.getMediaPath());
		} catch (IOException e) {
			return;
		}
		if (audioFolder.exists()) {
			File mediaFiles[] = audioFolder.listFiles();
			for (File f: mediaFiles) {
				f.delete();
			}
			audioFolder.delete();
		}

		File photoFolder;
		try {
			photoFolder = new File(getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + interview.getMediaPath());
		} catch (IOException e) {
			return;
		}
		if (photoFolder.exists()) {
			File mediaFiles[] = photoFolder.listFiles();
			for (File f: mediaFiles) {
				f.delete();
			}
			photoFolder.delete();
		}
	}

	/**
	 * Adds an new entry for an audio file to the database for the
	 * specified {@link Interview}. Returns an {@link InterviewAudio}
	 * object containing the {@link File} to use; other fields, including
	 * location and time fields are set to {@code null}.
	 * @param i The {@link Interview} to attach the new audio file to.
	 * @return An {@link InterviewAudio} object containing the new file.
	 * @throws IOException If external storage is not available for writing.
	 */
	public InterviewAudio newAudioForInterview(Interview i) throws IOException {
		checkExternalStorage();
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}

		InterviewAudio audio = new InterviewAudio();

		String audioPath = getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + i.getMediaPath();
		File audioDir = new File(audioPath);
		int count = audioDir.list().length;
		File newFile;
		String fileName;
		do  {
			do {
				// Add a the local filename to the absolute path, incrementing
				// the count until the file doesn't exist
				count++;
				fileName = "audio" + String.valueOf(count) + ".mp4";
				newFile = new File(audioPath + fileName);
			} while (newFile.exists());
			audio.setInterviewID(i.getID());
			audio.setAudioFile(newFile);
			audio.setFileName(fileName);
			// Keep looping if the database add fails (due to an entry whose file doesn't exist)
		} while (!dataSource.addAudio(audio));
		return audio;
	}

	/**
	 * Adds an new entry for an photo file to the database for the
	 * specified {@link Interview}. Returns an {@link InterviewPhoto}
	 * object containing the {@link File} to use; other fields, including
	 * location and time fields are set to {@code null}.
	 * @param i The {@link Interview} to attach the new photo file to.
	 * @return An {@link InterviewPhoto} object containing the new file.
	 * @throws IOException If external storage is not available for writing.
	 */
	public InterviewPhoto newPhotoForInterview(Interview i) throws IOException {
		checkExternalStorage();
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}
		InterviewPhoto photo = new InterviewPhoto();

		String photoPath = getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + i.getMediaPath();
		File photoDir = new File(photoPath);
		int count = photoDir.list().length;
		File newFile;
		String fileName;
		do {
			do {
				// Add a the local filename to the absolute path, incrementing
				// the count until the file doesn't exist
				count++;
				fileName = "photo" + String.valueOf(count) + ".jpg";
				newFile = new File(photoPath + fileName);
			} while (newFile.exists());
			photo.setInterviewID(i.getID());
			photo.setPhotoFile(newFile);
			photo.setFileName(fileName);
		} while (!dataSource.addPhoto(photo));
		return photo;
	}

	/**
	 * Saves the contents of the specified {@link InterviewAudio} file to the
	 * corresponding row in the database.
	 * @param audio The {@link InterviewAudio} object to write
	 * @return {@code true} if the operation succeeded, {@code false} if it
	 * failed.
	 */
	public boolean saveAudioFile(InterviewAudio audio) {
		if (audio == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}
		return dataSource.updateAudio(audio);
	}

	/**
	 * Saves the contents of the specified {@link InterviewPhoto} file to the
	 * corresponding row in the database.
	 * @param photo The {@link InterviewPhoto} object to write
	 * @return {@code true} if the operation succeeded, {@code false} if it
	 * failed.
	 */
	public boolean savePhotoFile(InterviewPhoto photo) {
		if (photo == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}
		return dataSource.updatePhoto(photo);
	}

	/**
	 * Returns a {@link List} of all audio files in the database associated with
	 * the specified {@link Interview}. If there are no associated files, the method
	 * will return an empty list.
	 * @param i The {@link Interview} to retrieve audio files for
	 * @return A {@link List} of {@link InterviewAudio} files associated with the
	 * specified {@code Interview}.
	 */
	public List<InterviewAudio> audioFilesForInterview(Interview i) {
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}

		List<InterviewAudio> list = dataSource.getAudioWithID(i.getID());

		for (InterviewAudio audio: list) {
			String path;
			try {
				path = getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + i.getMediaPath() + audio.getFileName();
			} catch (IOException e) {
				return null;
			}
			File file = new File(path);
			audio.setAudioFile(file);
			if (file.exists()) {
				audio.setFileStatus(InterviewFileStatus.FOUND);
			} else {
				audio.setFileStatus(InterviewFileStatus.NOT_FOUND);
			}
		}

		return list;
	}
	
	public InterviewAudio audioFileForInterview(Interview i, String name) {
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}

		InterviewAudio audio = dataSource.getAudioWithIDAndName(i.getID(), name);

		if (audio != null) {
			String path;
			try {
				path = getAppFolder(Environment.DIRECTORY_MUSIC).getAbsolutePath() + "/" + i.getMediaPath() + audio.getFileName();
			} catch (IOException e) {
				return null;
			}
			File file = new File(path);
			audio.setAudioFile(file);
			if (file.exists()) {
				audio.setFileStatus(InterviewFileStatus.FOUND);
			} else {
				audio.setFileStatus(InterviewFileStatus.NOT_FOUND);
			}
		}

		return audio;
	}

	/**
	 * Returns a {@link List} of all photo files in the database associated with
	 * the specified {@link Interview}. If there are no associated files, the method
	 * will return an empty list.
	 * @param i The {@link Interview} to retrieve photo files for
	 * @return A {@link List} of {@link InterviewPhoto} files associated with the
	 * specified {@code Interview}.
	 */
	public List<InterviewPhoto> photoFilesForInterview(Interview i) {
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}

		List<InterviewPhoto> list = dataSource.getPhotoWithID(i.getID());

		for (InterviewPhoto photo: list) {
			String path;
			try {
				path = getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + i.getMediaPath() + photo.getFileName();
			} catch (IOException e) {
				return null;
			}
			File file = new File(path);
			photo.setPhotoFile(file);
			if (file.exists()) {
				photo.setFileStatus(InterviewFileStatus.FOUND);
			} else {
				photo.setFileStatus(InterviewFileStatus.NOT_FOUND);
			}
		}

		return list;
	}

	/**
	 * Returns an {@link InterviewPhoto} object for a given {@link Interview} and
	 * file name. If there is no such photo, returns {@code null}.
	 * 
	 * @param i The {@link Interview} the photo is associated with.
	 * @param name The <i>local</i> file name of the photo, i.e. the value returned from
	 * {@code InterviewPhoto.getPhotoFile().getName()}.
	 * @return An {@code InterviewPhoto} object for the specified {@code Interview} and file
	 * name, or {@code null} if none exists.
	 */
	public InterviewPhoto photoFileForInterview(Interview i, String name) {
		if (i == null) {
			throw new IllegalArgumentException("Illegal null interview");
		}

		InterviewPhoto photo = dataSource.getPhotoWithIDAndName(i.getID(), name);

		if (photo != null) {
			String path;
			try {
				path = getAppFolder(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/" + i.getMediaPath() + photo.getFileName();
			} catch (IOException e) {
				return null;
			}
			File file = new File(path);
			photo.setPhotoFile(file);
			if (file.exists()) {
				photo.setFileStatus(InterviewFileStatus.FOUND);
			} else {
				photo.setFileStatus(InterviewFileStatus.NOT_FOUND);
			}
		}

		return photo;
	}

	/**
	 * Removes an audio file from the database, and deletes it from the filesystem.
	 * @param audio The audio file to delete.
	 * @return {@code true} if the delete succeeds, {@code false} if it fails.
	 * @throws IOException If external storage is unavailable for writing.
	 */
	public boolean removeAudioFile(InterviewAudio audio) throws IOException {
		checkExternalStorage();
		if (audio == null) {
			throw new IllegalArgumentException("Illegal null audio data");
		}
		audio.getAudioFile().delete();
		boolean result = dataSource.deleteAudio(audio);

		return result;
	}

	/**
	 * Removes a photo file from the database, and deletes it from the filesystem.
	 * @param photo The audio file to delete.
	 * @return {@code true} if the delete succeeds, {@code false} if it fails.
	 * @throws IOException If external storage is unavailable for writing.
	 */
	public boolean removePhotoFile(InterviewPhoto photo) throws IOException  {
		checkExternalStorage();
		if (photo == null) {
			throw new IllegalArgumentException("Illegal null photo data");
		}
		photo.getPhotoFile().delete();
		boolean result = dataSource.deletePhoto(photo);

		return result;
	}

	private static File getAppFolder(String folder) throws IOException {
		File publicFolder = Environment.getExternalStoragePublicDirectory(folder);
		String folderPath = publicFolder.getAbsolutePath() + "/" + APP_DIR;
		File appFolder = new File(folderPath);
		if (!appFolder.exists()) {
			appFolder.mkdirs();
		}
		File noMedia = new File(folderPath + ".nomedia");
		if (!noMedia.exists()) {
			noMedia.createNewFile();
		}
		return appFolder;
	}

	@Override
	public void finalize() {
		dataSource.close();
	}

	private static void checkExternalStorage() throws FileNotFoundException {
		if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
			throw new FileNotFoundException("External storage not mounted");
		}
		if (!Environment.getExternalStorageDirectory().canWrite()) {
			throw new FileNotFoundException("External storage not writable");
		}
	}

}
